﻿using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace UdpSubscriber
{
    sealed class PubReceivedEventArgs : EventArgs
    {
        private byte[] m_pubContent;
        private int m_length;// reading always from position zero

        public PubReceivedEventArgs(byte[] content, int length)
        {
            m_pubContent = content;
            m_length = length;
        }

        public byte[] PubContent { get { return m_pubContent; } }
        public int Length { get { return m_length; } }
    }

    sealed class UdpSubscriber : IDisposable
    {
        //**************************************************//
        #region [ member variables ]

        private Socket m_socket;
        private IPEndPoint m_pubEndpoint;
        private byte[] m_recvBuffer = new byte[1024];
        private bool m_disposed = false;

        #endregion

        //**************************************************//
        #region [ constructor and destructor ]

        public UdpSubscriber(string pubAddress, int pubPort)
        {
            m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            m_pubEndpoint = CreateEndPoint(pubAddress, pubPort);
        }

        ~UdpSubscriber()
        {
            Dispose(false);
        }

        #endregion

        //**************************************************//
        #region [ events ]

        public event EventHandler<PubReceivedEventArgs> PubMsgReceived;

        #endregion

        //**************************************************//
        #region [ public member methods ]

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void Start()
        {
            // first subscribe
            byte[] subCmd = Encoding.ASCII.GetBytes("subscribe");
            m_socket.SendTo(subCmd, m_pubEndpoint);

            // then post an asynchronous read to receive content published out
            Read();
        }

        #endregion

        //**************************************************//
        #region [ private member methods ]

        private IPEndPoint CreateEndPoint(string hostNameOrAddress, int port)
        {
            if (string.IsNullOrEmpty(hostNameOrAddress))
            {
                return new IPEndPoint(IPAddress.Any, port);
            }
            else
            {
                IPAddress address;
                if (IPAddress.TryParse(hostNameOrAddress, out address))
                    // Use the provided IP address.
                    return new IPEndPoint(address, port);
                else
                    // Exception will occur if DNS lookup fails.
                    return new IPEndPoint(Dns.GetHostEntry(hostNameOrAddress).AddressList[0], port);
            }
        }

        private void Dispose(bool isDisposing)
        {
            if (m_disposed)
                return;

            try
            {
                if (isDisposing)
                {
                    byte[] unsubCmd = Encoding.ASCII.GetBytes("unsubscribe");
                    m_socket.SendTo(unsubCmd,m_pubEndpoint);
                    m_socket.Close();
                }
            }
            finally
            {
                m_disposed = true;
            }
        }

        private void Read()
        {
            EndPoint remoteEndpoint = new IPEndPoint(IPAddress.Any, 0);// temporary usage
            m_socket.BeginReceiveFrom(m_recvBuffer, 0, m_recvBuffer.Length, SocketFlags.None, ref remoteEndpoint, OnReceivedCallback, null);
        }

        private void OnReceivedCallback(IAsyncResult asyncResult)
        {
            // end read
            EndPoint remoteEndpoint = new IPEndPoint(IPAddress.Any, 0);
            int recvLength = m_socket.EndReceiveFrom(asyncResult, ref remoteEndpoint);

            // publish the new-received message to consumers
            // !!!!!!!!!!!! it depends on the consumer to do the choice whether to copy the buffer
            // !!!!!!!!!!!! if the buffer will be used in another thread (such as Producer/Consumer pattern)
            // !!!!!!!!!!!! it is the responsibility of the consumer to copy the buffer
            OnPubMessageReceived(m_recvBuffer,recvLength);

            // post another asynchronous read to read next message
            Read();
        }

        private void OnPubMessageReceived(byte[] content,int length)
        {
            if (PubMsgReceived != null)
                PubMsgReceived(this,new PubReceivedEventArgs(content,length));
        }

        #endregion
    }
}
